using System;
using System.Collections.Generic;
using System.Text;
using Barrage;
using Barrage.Global;
using Barrage.Shaders;
using SlimDX;
using SlimDX.Direct3D9;
using System.Runtime.InteropServices;

namespace Barrage.Meshes
{
    /// <summary>
    /// Struct holding geometry instancing data (namely, a transformation matrix divided in 4 vectors)
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct InstanceData
    {
        /// <summary>
        /// Transformation matrix's first row
        /// </summary>
        public Vector4 _M1;
        /// <summary>
        /// Transformation matrix's second row
        /// </summary>
        public Vector4 _M2;
        /// <summary>
        /// Transformation matrix's third row
        /// </summary>
        public Vector4 _M3;
        /// <summary>
        /// Transformation matrix's fourth row
        /// </summary>
        public Vector4 _M4;

        /// <summary>
        /// Gets/Sets the transformation matrix of this instance
        /// </summary>
        public Matrix Matrix
        {
            set
            {
                _M1 = new Vector4(value.M11, value.M12, value.M13, value.M14);
                _M2 = new Vector4(value.M21, value.M22, value.M23, value.M24);
                _M3 = new Vector4(value.M31, value.M32, value.M33, value.M34);
                _M4 = new Vector4(value.M41, value.M42, value.M43, value.M44);
            }
            get
            {
                Matrix m=new Matrix();
                m.M11 = _M1.X; m.M12 = _M1.Y; m.M13 = _M1.Z; m.M14 = _M1.W;
                m.M21 = _M2.X; m.M22 = _M2.Y; m.M23 = _M2.Z; m.M24 = _M2.W;
                m.M31 = _M3.X; m.M32 = _M3.Y; m.M33 = _M3.Z; m.M34 = _M3.W;
                m.M41 = _M4.X; m.M42 = _M4.Y; m.M43 = _M4.Z; m.M44 = _M4.W;
                return m;
            }
        }
        
        /// <summary>
        /// Returns the size in bytes of this structure
        /// </summary>
        public static int SizeBytes
        {
            get { return Marshal.SizeOf(typeof(InstanceData)); }
        }

        /// <summary>
        /// Returns a vertex declaration of the elements of this structure (in the second stream)
        /// </summary>
        public static VertexElement[] Elements = new VertexElement[]{
                            new VertexElement(1,0,DeclarationType.Float4,DeclarationMethod.Default,DeclarationUsage.TextureCoordinate,1),
                            new VertexElement(1,16,DeclarationType.Float4,DeclarationMethod.Default,DeclarationUsage.TextureCoordinate,2),
                            new VertexElement(1,32,DeclarationType.Float4,DeclarationMethod.Default,DeclarationUsage.TextureCoordinate,3),
                            new VertexElement(1,48,DeclarationType.Float4,DeclarationMethod.Default,DeclarationUsage.TextureCoordinate,4),
                            VertexElement.VertexDeclarationEnd
                        };

        /// <summary>
        /// Inits an instance data
        /// </summary>
        /// <param name="m1">First matrix row</param>
        /// <param name="m2">Second matrix row</param>
        /// <param name="m3">Third matrix row</param>
        /// <param name="m4">Fourth matrix row</param>
        public InstanceData(Vector4 m1, Vector4 m2, Vector4 m3, Vector4 m4)
        {
            this._M1 = m1;
            this._M2 = m2;
            this._M3 = m3;
            this._M4 = m4;
        }

        /// <summary>
        /// Inits an instance data
        /// </summary>
        /// <param name="matrix">Transformation matrix</param>
        public InstanceData(Matrix matrix)
        {
            this._M1 = Vector4.Zero;
            this._M2 = Vector4.Zero;
            this._M3 = Vector4.Zero;
            this._M4 = Vector4.Zero;
            Matrix = matrix;
        }
    }

    struct DrawPart
    {
        public int Begin;
        public int Count;
        public BoundingSphere Sphere;
    }
    
    /// <summary>
    /// InstancedMesh allows the use of geometry instancing with a mesh loaded from a .x file.
    /// This can be done by using some special vertex declarations (added to the standard mesh declaration) and a specially done vertex shader
    /// By default it uses InstancingShader as a shader, but others can be used if they're prepared for geometry instancing
    /// </summary>
    public class InstancedMesh : Mesh
    {
        /// <summary>
        /// Instances buffer size
        /// </summary>
        public const int MAX_INSTANCES = 65536;
        /// <summary>
        /// Max number of instances that can be rendered at once. Defaults to 10240
        /// </summary>
        protected int groupSize = 10240;
        /// <summary>
        /// Gets/Sets the max number of instances that can be rendered at once. Defaults to 10240
        /// </summary>
        public int GroupSize { get { return groupSize; } set { groupSize = Math.Min(MAX_INSTANCES, value); } }

        /// <summary>
        /// Instance mesh
        /// </summary>
        protected SlimDX.Direct3D9.Mesh mesh = null;
        /// <summary>
        /// Bounding Sphere of one single instance
        /// </summary>
        protected BoundingSphere meshSphere;

        /// <summary>
        /// Buffer with instance data
        /// </summary>
        protected VertexBuffer instanceBuffer = null;

        /// <summary>
        /// Buffer with mesh data
        /// </summary>
        protected VertexBuffer meshBuffer = null;
        /// <summary>
        /// Buffer with mesh indices
        /// </summary>
        protected IndexBuffer meshIndexBuffer = null;

        int instanceNumber = 0;

        InstanceData[] instanceData = new InstanceData[MAX_INSTANCES];
        List<DrawPart> drawParts = new List<DrawPart>();

        /// <summary>
        /// Array with information of each instance
        /// </summary>
        public InstanceData[] InstanceData { get { return instanceData; } }

        VertexDeclaration declaration;
        
        /// <summary>
        /// Inits the instance buffer, that will hold information of every instance of the mesh drawn
        /// </summary>
        protected void initInstanceBuffer()
        {
            instanceBuffer = new VertexBuffer(Engine.Device, Barrage.Meshes.InstanceData.SizeBytes * MAX_INSTANCES, Usage.WriteOnly | Usage.Dynamic, VertexFormat.None, Pool.Default);
        }

        /// <summary>
        /// Adds an instance to the instance buffer
        /// </summary>
        public void AddInstance(InstanceData data)
        {
            if (instanceNumber >= MAX_INSTANCES) return;
            instanceData[instanceNumber++] = data;
        }

        /// <summary>
        /// Sets the instance count to 0
        /// </summary>
        public void ClearInstances()
        {
            instanceNumber = 0;
        }

        /// <summary>
        /// Updates the instance buffer data and the bounding spheres
        /// </summary>
        public void UpdateData()
        {
            DataStream stream = instanceBuffer.Lock(0, instanceNumber * Barrage.Meshes.InstanceData.SizeBytes, LockFlags.Discard);
            stream.WriteRange(instanceData, 0, instanceNumber);
            instanceBuffer.Unlock();
            UpdateParts();
        }

        /// <summary>
        /// Updates bounding spheres of each draw group to check frustum culling
        /// </summary>
        protected void UpdateParts()
        {
            drawParts.Clear();
            // Create draw parts
            int begin = 0;
            int remaining = instanceNumber;
            while (remaining > 0)
            {
                DrawPart p = new DrawPart();
                p.Begin = begin;
                p.Count = Math.Min(remaining, groupSize);
                BoundingSphere bs = ReplaceSphere(meshSphere,instanceData[begin]);
                for (int i=begin+1;i<begin+p.Count;i++)
                    bs=BoundingSphere.Merge(bs,ReplaceSphere(meshSphere,instanceData[i]));

                p.Sphere = bs;
                drawParts.Add(p);
                
                begin += p.Count;
                remaining -= p.Count;
            }
        }

        /// <summary>
        /// Transforms the mesh bounding sphere to match the instance transformation data
        /// </summary>
        /// <param name="sphere">Original mesh sphere</param>
        /// <param name="data">Instance data</param>
        /// <returns>The instance bounding sphere</returns>
        protected BoundingSphere ReplaceSphere(BoundingSphere sphere,InstanceData data)
        {
            Vector3 scale;
            Quaternion rotation;
            Vector3 translation;

            data.Matrix.Decompose(out scale,out rotation,out translation);
            return new BoundingSphere(sphere.Center+translation,sphere.Radius);
        }

        /// <summary>
        /// Constructor. Inits buffers and sets the default shader
        /// </summary>
        /// <param name="mesh">Mesh used for instancing</param>
        public InstancedMesh(SlimDX.Direct3D9.Mesh mesh)
        {
            this.mesh = mesh;
            mesh.OptimizeInPlace(MeshOptimizeFlags.Compact | MeshOptimizeFlags.VertexCache);
            
            foreach (ExtendedMaterial m in mesh.GetMaterials())
            {
                materials.Add(new Material(m));
            }
            mesh.SetMaterials(null);
            mesh.SetEffects(null);
            shader = InstancingShader.Instance;

            meshBuffer= mesh.VertexBuffer;
            meshIndexBuffer = mesh.IndexBuffer;

            meshSphere = XMesh.CreateBoundingSphere(mesh);

            initInstanceBuffer();

            initDeclaration();

            Engine.RegisterDisposeDefaultPoolHandler(new EngineDisposeHandler(DisposeInstanceBuffer));
            Engine.RegisterRecreateDefaultPoolHandler(new EngineDisposeHandler(initInstanceBuffer));
        }

        /// <summary>
        /// Inits the vertex declaration, adding to the mesh declaration the geometry instancing declarations
        /// </summary>
        protected void initDeclaration(){

            VertexElement [] meshElements = mesh.GetDeclaration();
            VertexElement [] instanceElements = Barrage.Meshes.InstanceData.Elements;

            VertexElement [] combinedElements = new VertexElement[meshElements.Length-1+instanceElements.Length];
            Array.Copy(meshElements,combinedElements,meshElements.Length-1);
            Array.Copy(instanceElements,0,combinedElements,meshElements.Length-1,instanceElements.Length);

            declaration= new VertexDeclaration(Engine.Device, combinedElements);
            
        }

        /// <summary>
        /// Constructor. Loads the mesh and inits the vertex buffers
        /// </summary>
        /// <param name="filename">Path to the mesh used for instancing</param>
        public InstancedMesh(string filename) : this(SlimDX.Direct3D9.Mesh.FromFile(Engine.Device, filename, MeshFlags.Managed)) { }

        /// <summary>
        /// \internal Renders the instances
        /// </summary>
        /// <param name="item">Item used to render this mesh</param>
        public override void Render(Barrage.Item item)
        {
            Engine.Device.SetStreamSourceFrequency(1, (int)StreamFrequency.InstanceData | 1 );
            foreach(DrawPart dp in drawParts){
                RenderStats.RenderCalls++;
                if (Scene.CurrentInstance.Camera.CheckCull(dp.Sphere))
                {
                    RenderStats.CulledObjects++;
                    continue;
                }
                Engine.Device.SetStreamSourceFrequency(0, (int)StreamFrequency.IndexedData | dp.Count);
                Engine.Device.VertexDeclaration = declaration;
                Engine.Device.SetStreamSource(1, instanceBuffer, Barrage.Meshes.InstanceData.SizeBytes * dp.Begin, Barrage.Meshes.InstanceData.SizeBytes);
                foreach (Material m in materials)
                {
                    shader.BeginRender(item, m);
                    int passNumber = shader.PassNumber(m);
                    for (int j = 0; j < passNumber; j++)
                    {
                        shader.BeginPass(j);
                        Engine.Device.SetStreamSource(0, meshBuffer, 0, mesh.BytesPerVertex);
                        Engine.Device.Indices = meshIndexBuffer;
                        Engine.Device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, mesh.VertexCount, 0, mesh.FaceCount);
                        shader.EndPass();
                    }
                    shader.EndRender();
                }
            }

            Engine.Device.SetStreamSourceFrequency(0, 1);
            Engine.Device.SetStreamSourceFrequency(1, 1);
            
            NLog.LogManager.GetLogger("Barrage::Scene::Meshes::XMesh").Debug("Rendering Mesh");
        }
        /// <summary>
        /// \internal Clear resources
        /// </summary>
        public override void Dispose()
        {
            Engine.UnregisterDisposeDefaultPoolHandler(new EngineDisposeHandler(DisposeInstanceBuffer));
            Engine.UnregisterRecreateDefaultPoolHandler(new EngineDisposeHandler(initInstanceBuffer));
            meshIndexBuffer.Dispose();
            meshIndexBuffer = null;
            meshBuffer.Dispose();
            meshBuffer = null;
            mesh.Dispose();
            mesh = null;
            DisposeInstanceBuffer();
            declaration.Dispose();
            declaration = null;
            base.Dispose();
        }

        void DisposeInstanceBuffer()
        {
            instanceBuffer.Dispose();
            instanceBuffer = null;
        }
    }
}
